//
//  YHFileManager.swift
//  FuweiCloud
//
//  Created by michael on 2024/10/11.
//

import AVFoundation
import CommonCrypto
import ImageIO
import MobileCoreServices
import UIKit

/*
 YHFileManager 实现的功能包括：
 文件和目录操作（创建、复制、移动、删除等）
 文件和目录属性读取
 目录内容列举
 文件读写操作
 路径处理和标准化
 系统目录路径获取
 图像元数据读取
 扩展属性（xattr）操作
 */
public class YHFileManager {
  // MARK: - Private Properties

  private static var absoluteDirectories: [String] = {
    let fileManager = FileManager.default
    let directories = [
      fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first,
      fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first,
      fileManager.urls(for: .documentDirectory, in: .userDomainMask).first,
      fileManager.urls(for: .libraryDirectory, in: .userDomainMask).first,
      Bundle.main.resourceURL,
      URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
    ].compactMap { $0?.path }
    return directories.sorted { $0.count > $1.count }
  }()

  // MARK: - Public Methods

  public static func absoluteDirectoryForPath(_ path: String) -> String? {
    assertPath(path)

    if path == "/" {
      return nil
    }

    for directory in absoluteDirectories {
      if path.hasPrefix(directory) {
        return directory
      }
    }

    return nil
  }

  public static func absolutePath(_ path: String) -> String {
    assertPath(path)

    if absoluteDirectoryForPath(path) != nil {
      return path
    } else {
      return pathForDocumentsDirectoryWithPath(path)
    }
  }

  public static func attribute(ofItemAtPath path: String, forKey key: FileAttributeKey) -> Any? {
    return try? FileManager.default.attributesOfItem(atPath: absolutePath(path))[key]
  }

  public static func attributes(ofItemAtPath path: String) -> [FileAttributeKey: Any]? {
    return try? FileManager.default.attributesOfItem(atPath: absolutePath(path))
  }

  public static func copyItem(atPath path: String, toPath: String, overwrite: Bool = false) throws {
    let absoluteToPath = absolutePath(toPath)

    if FileManager.default.fileExists(atPath: absoluteToPath) {
      if overwrite {
        try FileManager.default.removeItem(atPath: absoluteToPath)
      } else {
        throw NSError(domain: "YHFileManagerError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Destination path already exists"])
      }
    }

    try createDirectories(forFileAtPath: absoluteToPath)
    try FileManager.default.copyItem(atPath: absolutePath(path), toPath: absoluteToPath)
  }

  public static func createDirectories(forFileAtPath path: String) throws {
    let directoryPath = (absolutePath(path) as NSString).deletingLastPathComponent
    try FileManager.default.createDirectory(atPath: directoryPath, withIntermediateDirectories: true, attributes: nil)
  }

  public static func createFile(atPath path: String, content: Any? = nil, overwrite: Bool = false) throws {
    let absolutePath = self.absolutePath(path)

    if FileManager.default.fileExists(atPath: absolutePath) {
      if overwrite {
        try FileManager.default.removeItem(atPath: absolutePath)
      } else {
        throw NSError(domain: "YHFileManagerError", code: 2, userInfo: [NSLocalizedDescriptionKey: "File already exists"])
      }
    }

    try createDirectories(forFileAtPath: absolutePath)

    if let content = content {
      try write(fileAtPath: absolutePath, content: content)
    } else {
      FileManager.default.createFile(atPath: absolutePath, contents: nil, attributes: nil)
    }
  }

  public static func creationDate(ofItemAtPath path: String) -> Date? {
    return attribute(ofItemAtPath: path, forKey: .creationDate) as? Date
  }

  public static func modificationDate(ofItemAtPath path: String) -> Date? {
    return attribute(ofItemAtPath: path, forKey: .modificationDate) as? Date
  }

  public static func emptyCachesDirectory() throws {
    try removeFilesInDirectory(atPath: pathForCachesDirectory())
  }

  public static func emptyTemporaryDirectory() throws {
    try removeFilesInDirectory(atPath: pathForTemporaryDirectory())
  }

  public static func exists(itemAtPath path: String) -> Bool {
    return FileManager.default.fileExists(atPath: absolutePath(path))
  }

  public static func isDirectory(itemAtPath path: String) -> Bool {
    var isDirectory: ObjCBool = false
    FileManager.default.fileExists(atPath: absolutePath(path), isDirectory: &isDirectory)
    return isDirectory.boolValue
  }

  public static func isFile(itemAtPath path: String) -> Bool {
    var isDirectory: ObjCBool = false
    let exists = FileManager.default.fileExists(atPath: absolutePath(path), isDirectory: &isDirectory)
    return exists && !isDirectory.boolValue
  }

  public static func isExecutable(itemAtPath path: String) -> Bool {
    return FileManager.default.isExecutableFile(atPath: absolutePath(path))
  }

  public static func isReadable(itemAtPath path: String) -> Bool {
    return FileManager.default.isReadableFile(atPath: absolutePath(path))
  }

  public static func isWritable(itemAtPath path: String) -> Bool {
    return FileManager.default.isWritableFile(atPath: absolutePath(path))
  }

  public static func listDirectories(inDirectoryAtPath path: String, deep: Bool = false) -> [String] {
    return listItems(inDirectoryAtPath: path, deep: deep).filter { isDirectory(itemAtPath: $0) }
  }

  public static func listFiles(inDirectoryAtPath path: String, deep: Bool = false) -> [String] {
    return listItems(inDirectoryAtPath: path, deep: deep).filter { isFile(itemAtPath: $0) }
  }

  public static func listItems(inDirectoryAtPath path: String, deep: Bool = false) -> [String] {
    let absolutePath = self.absolutePath(path)
    let enumerator = FileManager.default.enumerator(atPath: absolutePath)
    var items: [String] = []

    while let filePath = enumerator?.nextObject() as? String {
      let fullPath = (absolutePath as NSString).appendingPathComponent(filePath)
      items.append(fullPath)

      if !deep {
        enumerator?.skipDescendants()
      }
    }

    return items
  }

  public static func moveItem(atPath path: String, toPath: String, overwrite: Bool = false) throws {
    try copyItem(atPath: path, toPath: toPath, overwrite: overwrite)
    try removeItem(atPath: path)
  }

  public static func removeItem(atPath path: String) throws {
    try FileManager.default.removeItem(atPath: absolutePath(path))
  }

  public static func removeFilesInDirectory(atPath path: String) throws {
    let files = listFiles(inDirectoryAtPath: path)
    for file in files {
      try removeItem(atPath: file)
    }
  }

  public static func renameItem(atPath path: String, withName newName: String) throws {
    let absolutePath = self.absolutePath(path)
    let newPath = ((absolutePath as NSString).deletingLastPathComponent as NSString).appendingPathComponent(newName)
    try moveItem(atPath: absolutePath, toPath: newPath)
  }

  public static func size(ofItemAtPath path: String) -> Int64? {
    return (attribute(ofItemAtPath: path, forKey: .size) as? NSNumber)?.int64Value
  }

  public static func sizeFormatted(ofItemAtPath path: String) -> String? {
    guard let size = size(ofItemAtPath: path) else { return nil }
    return ByteCountFormatter.string(fromByteCount: size, countStyle: .file)
  }

  // MARK: - Directory Path Methods

  public static func pathForDocumentsDirectory() -> String {
    return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?.path ?? ""
  }

  public static func pathForDocumentsDirectoryWithPath(_ path: String) -> String {
    return (pathForDocumentsDirectory() as NSString).appendingPathComponent(path)
  }

  public static func pathForApplicationSupportDirectory() -> String {
    return FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first?.path ?? ""
  }

  public static func pathForCachesDirectory() -> String {
    return FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first?.path ?? ""
  }

  public static func pathForLibraryDirectory() -> String {
    return FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first?.path ?? ""
  }

  public static func pathForMainBundleDirectory() -> String {
    return Bundle.main.resourcePath ?? ""
  }

  public static func pathForTemporaryDirectory() -> String {
    return NSTemporaryDirectory()
  }

  // MARK: - Read Methods

  public static func readFileAtPath(_ path: String) -> String? {
    return try? String(contentsOfFile: absolutePath(path), encoding: .utf8)
  }

  public static func readFileAtPathAsData(_ path: String) -> Data? {
    return try? Data(contentsOf: URL(fileURLWithPath: absolutePath(path)))
  }

  public static func readFileAtPathAsImage(_ path: String) -> UIImage? {
    return UIImage(contentsOfFile: absolutePath(path))
  }

  // MARK: - Write Methods

  public static func writeFile(atPath path: String, content: Any) throws {
    try write(fileAtPath: absolutePath(path), content: content)
  }

  // MARK: - Image Metadata Methods

  public static func metadataOfImage(atPath path: String) -> [String: Any]? {
    guard let url = URL(string: absolutePath(path)),
          let source = CGImageSourceCreateWithURL(url as CFURL, nil),
          let metadata = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [String: Any]
    else {
      return nil
    }
    return metadata
  }

  public static func exifDataOfImage(atPath path: String) -> [String: Any]? {
    return metadataOfImage(atPath: path)?[kCGImagePropertyExifDictionary as String] as? [String: Any]
  }

  public static func tiffDataOfImage(atPath path: String) -> [String: Any]? {
    return metadataOfImage(atPath: path)?[kCGImagePropertyTIFFDictionary as String] as? [String: Any]
  }

  // MARK: - Extended Attribute (xattr) Methods

  public static func xattrOfItem(atPath path: String) -> [String: String] {
    let absolutePath = self.absolutePath(path)
    var dict = [String: String]()

    let bufferLength = listxattr(absolutePath, nil, 0, 0)
    guard bufferLength != -1 else { return dict }

    let buffer = UnsafeMutablePointer<Int8>.allocate(capacity: bufferLength)
    defer { buffer.deallocate() }

    let keyLength = listxattr(absolutePath, buffer, bufferLength, 0)
    guard keyLength != -1 else { return dict }

    var start = 0
    while start < keyLength {
      let key = String(cString: buffer.advanced(by: start))
      if let value = xattrOfItem(atPath: absolutePath, getValueForKey: key) {
        dict[key] = value
      }
      start += key.utf8.count + 1
    }

    return dict
  }

  public static func xattrOfItem(atPath path: String, getValueForKey key: String) -> String? {
    let absolutePath = self.absolutePath(path)
    let valueLength = getxattr(absolutePath, key, nil, 0, 0, 0)
    guard valueLength != -1 else { return nil }

    let buffer = UnsafeMutablePointer<Int8>.allocate(capacity: valueLength)
    defer { buffer.deallocate() }

    let result = getxattr(absolutePath, key, buffer, valueLength, 0, 0)
    guard result != -1 else { return nil }

    return String(cString: buffer)
  }

  public static func xattrOfItem(atPath path: String, hasValueForKey key: String) -> Bool {
    return xattrOfItem(atPath: path, getValueForKey: key) != nil
  }

  @discardableResult
  public static func xattrOfItem(atPath path: String, removeValueForKey key: String) -> Bool {
    let absolutePath = self.absolutePath(path)
    return removexattr(absolutePath, key, 0) != -1
  }

  @discardableResult
  public static func xattrOfItem(atPath path: String, setValue value: String, forKey key: String) -> Bool {
    let absolutePath = self.absolutePath(path)
    return setxattr(absolutePath, key, value, value.utf8.count, 0, 0) != -1
  }

  // MARK: - Compression Methods

  public static func compressFile(atPath path: String, to destinationPath: String) throws {
    let data = try Data(contentsOf: URL(fileURLWithPath: absolutePath(path)))
    let compressedData = data.compress(algorithm: .zlib)
    try compressedData.write(to: URL(fileURLWithPath: absolutePath(destinationPath)))
  }

  public static func decompressFile(atPath path: String, to destinationPath: String) throws {
    let data = try Data(contentsOf: URL(fileURLWithPath: absolutePath(path)))
    let decompressedData = data.decompress(algorithm: .zlib)
    try decompressedData.write(to: URL(fileURLWithPath: absolutePath(destinationPath)))
  }

  // MARK: - Hashing Methods

  public static func md5(ofFileAtPath path: String) -> String? {
    guard let data = try? Data(contentsOf: URL(fileURLWithPath: absolutePath(path))) else {
      return nil
    }
    return data.md5String
  }

  public static func sha256(ofFileAtPath path: String) -> String? {
    guard let data = try? Data(contentsOf: URL(fileURLWithPath: absolutePath(path))) else {
      return nil
    }
    return data.sha256String
  }

  // MARK: - File Type Detection

  public static func mimeType(ofFileAtPath path: String) -> String {
    let url = URL(fileURLWithPath: absolutePath(path))
    let pathExtension = url.pathExtension.lowercased()

    if let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension as CFString, nil)?.takeRetainedValue() {
      if let mimetype = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType)?.takeRetainedValue() {
        return mimetype as String
      }
    }
    return "application/octet-stream"
  }

  // MARK: - Batch Operations

  public static func performBatchOperation(_ operation: (String) throws -> Void, onItemsAtPaths paths: [String]) throws {
    for path in paths {
      try operation(path)
    }
  }

  // MARK: - Asynchronous Operations

  public static func readFileAsync(atPath path: String, completion: @escaping (Result<String, Error>) -> Void) {
    DispatchQueue.global(qos: .userInitiated).async {
      do {
        let content = try String(contentsOfFile: absolutePath(path), encoding: .utf8)
        AsyncOnMain {
          completion(.success(content))
        }
      } catch {
        AsyncOnMain {
          completion(.failure(error))
        }
      }
    }
  }

  public static func writeFileAsync(atPath path: String, content: String, completion: @escaping (Error?) -> Void) {
    DispatchQueue.global(qos: .userInitiated).async {
      do {
        try content.write(toFile: absolutePath(path), atomically: true, encoding: .utf8)
        AsyncOnMain {
          completion(nil)
        }
      } catch {
        AsyncOnMain {
          completion(error)
        }
      }
    }
  }

  // MARK: - Private Methods

  private static func assertPath(_ path: String) {
    precondition(!path.isEmpty, "Invalid path. Path cannot be empty string.")
  }

  private static func write(fileAtPath path: String, content: Any) throws {
    switch content {
    case let array as [Any]:
      (array as NSArray).write(toFile: path, atomically: true)
    case let data as Data:
      try data.write(to: URL(fileURLWithPath: path), options: .atomic)
    case let dictionary as [String: Any]:
      (dictionary as NSDictionary).write(toFile: path, atomically: true)
    case let string as String:
      try string.write(toFile: path, atomically: true, encoding: .utf8)
    case let image as UIImage:
      try image.pngData()?.write(to: URL(fileURLWithPath: path), options: .atomic)
    case let codable as Encodable:
      let data = try JSONEncoder().encode(codable)
      try data.write(to: URL(fileURLWithPath: path), options: .atomic)
    default:
      throw NSError(domain: "YHFileManagerError", code: 3, userInfo: [NSLocalizedDescriptionKey: "Unsupported content type"])
    }
  }
}


public extension YHFileManager {
  static func filePath(of path: String) -> String {
    if !isFile(itemAtPath: path) {
      try? createFile(atPath: path)
    }
    return path
  }

  static func directoryPath(of path: String) -> String {
    if !isDirectory(itemAtPath: path) {
      try? createDirectories(forFileAtPath: path)
    }
    return path
  }
}
